#region Licence
// Copyright (c) 2008 Tasman Logic Ltd

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
// and associated documentation files (the "Software"), to deal in the Software without restriction, 
// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
// subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies 
// or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 
// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using ActiveSharp.PropertyMapping;
using ActiveSharp.Utils;

namespace ActiveSharp.Inspection
{
    // not used: public delegate void InstructionInspectionDelegate(Instruction current);

    /// <summary>
    /// Allows us to inspect the MSIL instructions in a method
    /// </summary>
    /// <remarks>Possible applications include:
    /// - mapping fields to properties (as in <see cref="FieldToPropertyMap"/>
    /// - analysing dependencies between methods (call graphs)
    /// - ...</remarks>
    public class MethodInspector
    {
        public MethodInspector(MethodBase targetMethod):this(targetMethod, true) // be default, do the old behaviour, which is to throw if method has no MSIL
        {}

        public MethodInspector(MethodBase targetMethod, bool throwIfMethodHasNoMsil)
        {
            TargetMethod = targetMethod;
            Instructions = ParseInstructions(targetMethod, throwIfMethodHasNoMsil);
        }

        public readonly MethodBase TargetMethod;
        public readonly List<Instruction> Instructions;

        /// <summary>
        /// Returns the list of instructions that make up <paramRef name="targetMethod"/>
        /// </summary>
        private static List<Instruction> ParseInstructions(MethodBase targetMethod, bool throwIfMethodHasNoMsil)
        {
            // For a good description of MSIL inspection, see: http://blogs.msdn.com/haibo_luo/archive/2005/10/02/476242.aspx, which was used as a reference in the code below
            // The same author has posted a visitor-based alternative to this code here (http://blogs.msdn.com/haibo_luo/archive/2006/11/06/system-reflection-based-ilreader.aspx) although, for now, I'm sticking with the simple approach here
            List<Instruction> result = new List<Instruction>();

            MethodBody bodyInfo = targetMethod.GetMethodBody();    //will be null for internal CLR methods, e.g. Type.GetType()
            if (bodyInfo == null)
            {
                if (throwIfMethodHasNoMsil)
                {
                    throw new ArgumentException("MSIL method body is not available for method '" + targetMethod.DeclaringType.FullName + "." + targetMethod.Name + "'");
                }
                else
                {
                    // just return an empty list of instructions
                    // This may happen for abstract methods, native methods, and methods on compiler-generated delegates (which are just a special case of native methods I suspect)
                    return new List<Instruction>();
                }
            }


            byte[] msil = bodyInfo.GetILAsByteArray();

            int position = 0;
            while (position < msil.Length)
            {
                //read 1 or 2 byte opcode.  Our Opcode lists contain Nullable<OpCode>, to ensure that if there is an error in indexing them, where we end up with something that's completely out of range (i.e. an un-used cell in the array) we will get an exception (since HasValue will be false in that case)
                OpCode op;
                byte byteFromMsil = msil[position++];
                if (byteFromMsil != 0xFE)                                    // all double byte opcodes start with FE
                    op = OpCodeLists.SingleByte[byteFromMsil].Value;
                else
                    op = OpCodeLists.DoubleByte[msil[position++]].Value;
                 
                // read operand bytes
                int operandLength = GetOperandLength(op, msil, position);
                byte[] operandBytes = new byte[operandLength];
                Array.Copy(msil, position, operandBytes, 0, operandLength);
                position += operandLength;

                // Create Instruction object
                Instruction ins = new Instruction(targetMethod, op, operandBytes);
                ins.AddToList(result);
            }
            return result;
        }

        /// <summary>
        /// .NET operands have varying lengths, so we need to figure out the length in each particular case
        /// </summary>
        static int GetOperandLength(OpCode op, byte[] msil, int position)
        {
            switch (op.OperandType)
            {
                case OperandType.InlineSwitch:
                {
                    UInt32 numberOfCases = BitConverter.ToUInt32(msil, position);
                    return (int)(4 * (1 + numberOfCases));                                 //add 1 to include the 32-bit case-count    
                }

                case OperandType.InlineNone:
                    return 0;

                case OperandType.ShortInlineBrTarget:
                case OperandType.ShortInlineI:
                case OperandType.ShortInlineVar:
                    return 1;

                case OperandType.InlineVar:
                    return 2;

                case OperandType.InlineBrTarget:
                case OperandType.InlineTok:
                case OperandType.InlineType:
                case OperandType.InlineSig:
                case OperandType.InlineString:
                case OperandType.InlineField:
                case OperandType.InlineI:
                case OperandType.InlineMethod:
                case OperandType.ShortInlineR:
                    return 4;

                case OperandType.InlineI8:
                case OperandType.InlineR:
                    return 8;

                default:
                {
                    throw new UnexpectedCaseException();
                }
            }
        }

        #region Helper members, for parsing MSIL for other purposes

        public IList<string> AllStringLiterals
        {
            get
            {
                var result = new List<string>();
                foreach (var current in Instructions)
                    if (current.OpCode == OpCodes.Ldstr)
                        result.Add(current.Operand.AsString);
                return result;
            }
        }

        /// <summary>
        /// Does the inspected method contain the given string literal
        /// </summary>
        public bool ContainsStringLiteral(string s)
        {
            return AllStringLiterals.Contains(s);
        }

        public IList<MethodBase> AllCalledMethods
        {
            get
            {
                var result = new List<MethodBase>();
                foreach (var current in Instructions)
                    if (current.OpCode.FlowControl == FlowControl.Call)
                        result.Add(current.Operand.AsMethod);
                return result;
            }
        }

        /// <summary>
        /// Does the inspected method contain a call to <paramref name="m"/>
        /// </summary>
        /// <remarks>If method is virtual, pass in the MethodInfo for the base method (i.e. the one with "virtual" or "abstract" on it)</remarks>
        public bool ContainsCallTo(MethodBase m)
        {
            foreach (var test in AllCalledMethods)
                if (MemberInfoUtil.AreEqual(test, m))
                    return true;

            return false;
        }

        #endregion
    }
}
